﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="NHibernateSession.cs" company="">
//   
// </copyright>
// <summary>
//   Defines the NHibernateSession type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

#region

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Validator.Cfg;
using NHibernate.Validator.Engine;
using SharpArch.Core;

#endregion

namespace SharpArch.Data.NHibernate
{
    /// <summary>
    /// classe nhibernate session
    /// </summary>
    public static class NHibernateSession
    {
        #region Init() overloads
        /// <summary>
        /// initialisation de la config
        /// </summary>
        /// <param name="storage">session storage</param>
        /// <param name="mappingAssemblies">mapping assemblies</param>
        /// <returns>la configuration</returns>
        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies)
        {
            return Init(storage, mappingAssemblies, null, null, null, null, null);
        }

        /// <summary>
        /// initialisation de la config
        /// </summary>
        /// <param name="storage">session storage</param>
        /// <param name="mappingAssemblies">mapping assemblies</param>
        /// <param name="cfgFile">config file</param>
        /// <returns>retourne une configuration</returns>
        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, string cfgFile)
        {
            return Init(storage, mappingAssemblies, null, cfgFile, null, null, null);
        }

        /// <summary>
        /// initialisation de la config
        /// </summary>
        /// <param name="storage">session storage</param>
        /// <param name="mappingAssemblies">mapping assemblies</param>
        /// <param name="cfgProperties">cfg properties</param>
        /// <returns>retounr la configuration</returns>
        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, IDictionary<string, string> cfgProperties)
        {
            return Init(storage, mappingAssemblies, null, null, cfgProperties, null, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, string cfgFile, 
                                         string validatorCfgFile)
        {
            return Init(storage, mappingAssemblies, null, cfgFile, null, validatorCfgFile, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, 
                                         AutoPersistenceModel autoPersistenceModel)
        {
            return Init(storage, mappingAssemblies, autoPersistenceModel, null, null, null, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, 
                                         AutoPersistenceModel autoPersistenceModel, string cfgFile)
        {
            return Init(storage, mappingAssemblies, autoPersistenceModel, cfgFile, null, null, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, 
                                         AutoPersistenceModel autoPersistenceModel, 
                                         IDictionary<string, string> cfgProperties)
        {
            return Init(storage, mappingAssemblies, autoPersistenceModel, null, cfgProperties, null, null);
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, 
                                         AutoPersistenceModel autoPersistenceModel, string cfgFile, 
                                         string validatorCfgFile)
        {
            return Init(storage, mappingAssemblies, autoPersistenceModel, cfgFile, null, validatorCfgFile, null);
        }

        #endregion

        /// <summary>
        /// The default factory key used if only one database is being communicated with.
        /// </summary>
        public static readonly string DefaultFactoryKey = "nhibernate.current_session";

        private static IDictionary<string, ISessionFactory> _sessionFactories =
            new Dictionary<string, ISessionFactory>();

        private static IDictionary<string, ISessionStorage> _storages = new Dictionary<string, ISessionStorage>();
        private static IInterceptor RegisteredInterceptor;

        public static Configuration LastConfiguration { get; private set; }

        /// <summary>
        /// Provides access to the <see cref="ISessionFactory" /> assuming you're communicating
        /// with one database; otherwise, you should use <see cref="SessionFactories" />.
        /// </summary>
        public static ISessionFactory SessionFactory
        {
            get
            {
                Check.Require(SessionFactories.Count <= 1, "The SessionFactory getter may only " +
                                                           "be invoked if you only have zero or one NHibernate session factories; i.e., " +
                                                           "you're only communicating with one database or have not yet set the " +
                                                           "SessionFactory.  Since you've configured multiple SesionFactores for " +
                                                           "communications with multiple databases, you should instead use SessionFactories");

                return SessionFactories.Values.FirstOrDefault();
            }

            set
            {
                Check.Require(SessionFactories.Count <= 1, 
                              "The SessionFactory setter may only be invoked if you have not yet set an NHibernate " +
                              "session factory or are reassigning the one, and only one, that exists; i.e., you're only " +
                              "communicating with one database.  Since you've already created more than one session factory, " +
                              "you should instead use the SessionFactories property.");

                if (value != null)
                {
                    if (SessionFactories.Count == 0)
                    {
                        SessionFactories.Add(DefaultFactoryKey, value);
                    }
                        
// Replace existing session factory if different
                    else if (! value.Equals(SessionFactories.First()))
                    {
                        RemoveOneAndOnlySessionFactory();
                        SessionFactories.Add(DefaultFactoryKey, value);
                    }
                }
                else
                {
                    if (SessionFactories.Count > 0)
                    {
                        RemoveOneAndOnlySessionFactory();
                    }
                }
            }
        }

        public static ISessionStorage Storage
        {
            get
            {
                Check.Require(Storages.Count <= 1, "The Storage getter may only " +
                                                   "be invoked if you only have zero or one NHibernate session storages; i.e., " +
                                                   "you're only communicating with one database or have not yet set the " +
                                                   "Storage.  Since you've configured multiple Storages for " +
                                                   "communications with multiple databases, you should instead use the Storages property.");

                return Storages.Values.FirstOrDefault();
            }

            set
            {
                Check.Require(Storages.Count <= 1, 
                              "The Storages setter may only be invoked if you have not yet set an NHibernate " +
                              "session storage or reassigning the one, and only one, that exists; i.e., you're only " +
                              "communicating with one database. Since you've already created more than one session storage, " +
                              "you should instead use the Storages property.");

                if (value != null)
                {
                    if (Storages.Count == 0)
                    {
                        Storages.Add(DefaultFactoryKey, value);
                    }
                        
// Replace existing session storage if different
                    else if (!value.Equals(Storages.First()))
                    {
                        RemoveOneAndOnlySessionStorage();
                        Storages.Add(DefaultFactoryKey, value);
                    }
                }
                else
                {
                    if (Storages.Count > 0)
                    {
                        RemoveOneAndOnlySessionStorage();
                    }
                }
            }
        }

        /// <summary>
        /// Provides an access to configured<see cref="ValidatorEngine"/>.
        /// </summary>
        /// <value>The validator engine.</value>
        public static ValidatorEngine ValidatorEngine { get; set; }

        /// <summary>
        /// Used to get the current NHibernate session if you're communicating with a single database.
        /// When communicating with multiple databases, invoke <see cref="CurrentFor()" /> instead.
        /// </summary>
        public static ISession Current
        {
            get
            {
                Check.Require(Storages.Count == 1, "The NHibernateSession.Current property may " +
                                                   "only be invoked if you only have one NHibernate session factory; i.e., you're " +
                                                   "only communicating with one database.  Since you're configured communications " +
                                                   "with multiple databases, you should instead call Current(string factoryKey)");

                return CurrentFor(DefaultFactoryKey);
            }
        }

        /// <summary>
        /// Maintains a dictionary of NHibernate session factories, one per database.  The key is 
        /// the "factory key" used to look up the associated database, and used to decorate respective
        /// repositories.  If only one database is being used, this dictionary contains a single
        /// factory with a key of <see cref="DefaultFactoryKey" />.
        /// </summary>
        public static IDictionary<string, ISessionFactory> SessionFactories
        {
            get { return _sessionFactories; }

            set { _sessionFactories = value; }
        }

        /// <summary>
        /// Maintains a dictionary of NHibernate session storages, one per database.  The key is 
        /// the "factory key" used to look up the associated database, and used to decorate respective
        /// repositories.  If only one database is being used, this dictionary contains a single
        /// session storage with a key of <see cref="DefaultFactoryKey" />.
        /// </summary>
        public static IDictionary<string, ISessionStorage> Storages
        {
            get { return _storages; }

            set { _storages = value; }
        }

        public static Configuration Init(ISessionStorage storage, string[] mappingAssemblies, 
                                         AutoPersistenceModel autoPersistenceModel, string cfgFile, 
                                         IDictionary<string, string> cfgProperties, string validatorCfgFile, 
                                         IPersistenceConfigurer persistenceConfigurer)
        {
            Check.Require(storage != null, "storage mechanism was null but must be provided");
            Check.Require(!SessionFactories.ContainsKey(storage.FactoryKey), 
                          "A session factory has already been configured with the key of " + storage.FactoryKey);

            Configuration cfg = ConfigureNHibernate(cfgFile, cfgProperties);
            LastConfiguration = cfg;
            ConfigureNHibernateValidator(cfg, validatorCfgFile);

            SessionFactories.Add(
                storage.FactoryKey, 
                CreateSessionFactoryFor(mappingAssemblies, autoPersistenceModel, cfg, persistenceConfigurer));

            Storages.Add(storage.FactoryKey, storage);

            return cfg;
        }

        private static ISessionFactory CreateSessionFactoryFor(string[] mappingAssemblies, 
                                                               AutoPersistenceModel autoPersistenceModel, 
                                                               Configuration cfg, 
                                                               IPersistenceConfigurer persistenceConfigurer)
        {
            FluentConfiguration fluentConfiguration = Fluently.Configure(cfg);

            if (persistenceConfigurer != null)
            {
                fluentConfiguration.Database(persistenceConfigurer);
            }

            fluentConfiguration.Mappings(m =>
                                             {
                                                 foreach (var mappingAssembly in mappingAssemblies)
                                                 {
                                                     var assembly =
                                                         Assembly.LoadFrom(MakeLoadReadyAssemblyName(mappingAssembly));

                                                     m.HbmMappings.AddFromAssembly(assembly);
                                                     m.FluentMappings.AddFromAssembly(assembly)
                                                         .Conventions.AddAssembly(assembly);
                                                 }

                                                 if (autoPersistenceModel != null)
                                                 {
                                                     m.AutoMappings.Add(autoPersistenceModel);

                                                     

                                                     string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 
                                                                                "hbm");
                                                     if (Directory.Exists(path))
                                                     {
                                                         m.AutoMappings.ExportTo(path);
                                                     }

                                                     
                                                 }
                                             });

            return fluentConfiguration.BuildSessionFactory();
        }

        public static void RegisterInterceptor(IInterceptor interceptor)
        {
            Check.Require(interceptor != null, "interceptor may not be null");

            RegisteredInterceptor = interceptor;
        }

        private static void RemoveOneAndOnlySessionFactory()
        {
            Check.Require(SessionFactories.Count <= 1, "This may only be invoked if SessionFactories " +
                                                       "contains 1 or fewer items; it currently contains " +
                                                       SessionFactories.Count);

            if (SessionFactories.Count > 0)
            {
                ISessionFactory factory = SessionFactories.Values.First();
                factory.Dispose();
                factory = null;
                SessionFactories.Clear();
            }
        }

        private static void RemoveOneAndOnlySessionStorage()
        {
            Check.Require(Storages.Count <= 1, "This may only be invoked if Storages " +
                                               "contains 1 or fewer items; it currently contains " + Storages.Count);

            if (Storages.Count > 0)
            {
                // ISessionStorage storage = Storages.Values.First();
                // storage = null;
                Storages.Clear();
            }
        }

        public static bool IsConfiguredForMultipleDatabases()
        {
            return SessionFactories.Count > 1;
        }

        /// <summary>
        /// Used to get the current NHibernate session associated with a factory key; i.e., the key 
        /// associated with an NHibernate session factory for a specific database.
        /// 
        /// If you're only communicating with one database, you should call <see cref="Current"/> instead,
        /// although you're certainly welcome to call this if you have the factory key available.
        /// </summary>
        /// <param name="factoryKey">
        /// The factory Key.
        /// </param>
        public static ISession CurrentFor(string factoryKey)
        {
            Check.Require(! string.IsNullOrEmpty(factoryKey), "factoryKey may not be null or empty");
            Check.Require(Storages.ContainsKey(factoryKey), 
                          "An ISessionStorage does not exist with a factory key of " + factoryKey);
            Check.Require(SessionFactories.ContainsKey(factoryKey), 
                          "An ISessionFactory does not exist with a factory key of " + factoryKey);

            ISession session = Storages[factoryKey].Session;

            if (session == null)
            {
                if (RegisteredInterceptor != null)
                {
                    session = SessionFactories[factoryKey].OpenSession(RegisteredInterceptor);
                }
                else
                {
                    session = SessionFactories[factoryKey].OpenSession();
                }

                Storages[factoryKey].Session = session;
            }

            return session;
        }

        private static string MakeLoadReadyAssemblyName(string assemblyName)
        {
            return (assemblyName.IndexOf(".dll") == -1)
                       ? assemblyName.Trim() + ".dll"
                       : assemblyName.Trim();
        }

        private static Configuration ConfigureNHibernate(string cfgFile, IDictionary<string, string> cfgProperties)
        {
            Configuration cfg = new Configuration();

            if (cfgProperties != null)
                cfg.AddProperties(cfgProperties);

            if (string.IsNullOrEmpty(cfgFile))
                return cfg.Configure();

            return cfg.Configure(cfgFile);
        }

        private static void ConfigureNHibernateValidator(Configuration cfg, string validatorCfgFile)
        {
            ValidatorEngine engine = new ValidatorEngine();

            if (string.IsNullOrEmpty(validatorCfgFile))
                engine.Configure();
            else
                engine.Configure(validatorCfgFile);

            // Register validation listeners with the current NHib configuration
            ValidatorInitializer.Initialize(cfg, engine);

            ValidatorEngine = engine;
        }
    }
}